android so层解密分析

android so层解密分析
grand参考
Android so层逆向分析入门 - Curz0n’s Blog
ida为9版本
so分析
静态注册的方法可以直接在导出表查询到 动态注册则是需要查看jni_onload函数进行分析
stringFromJNI为静态注册
test方法为动态注册
jni_onload第一个参数的类型是JavaVM* 面对错误可以手动修复
动态注册流程
1 | //第一步,实现JNI_OnLoad方法 |
gMethods为JNINativeMethod结构体
1 | typedef struct { |
跟进第一个函数 同样修改数据类型
跟进第二个函数 修改a1变量 由于a1是第一个函数的返回值 所以是jnienv的数据类型
那么对应的第三个函数就是注册函数 再次修复
init段
在链接so共享目标文件的时候,如果so中存在.init和.init_array段,则会先执行.init和.init_array段的函数,然后再执行JNI_OnLoad函数。通过静态分析可知,JNI_OnLoad函数中的v4指针指向的地址上的变量值是加密状态,在实际运行的过程中,v4指针指向的地址上的值应该是解密状态,所以解密的操作应该在JNI_OnLoad函数运行之前,.init或者.init_array段上的函数。
查看Segments视图(快捷键Ctrl+S),该目标文件只存在.init_array段:
发现异或的东西 要么调试so文件 要么手动修
这里可以解出注册方法在apk中的test方法
JNINativeMethod结构体的第三个成员指向实现Java方法的C/C++函数地址,so文件的.data段一般是保存已经初始化的全局静态变量和局部变量,动态注册函数的信息一般存放在.data.rel.ro.local
段。在IDA View视图选中byte_1C066或者byte_1C070变量,交叉引用(快捷键X)跳转到.data.rel.ro段
跟进找到一个处理函数
继续跟进 这里的format参数也是需要进行解密才能知道是什么
1 | data=[0xDA, 0x85, 0x87, 0x9A, 0x96, 0xDA, 0xD0, 0x91, 0xDA, 0x98, |
根据解密以后的字符串可以知道这个地方就是获取libnative-lib.so的加载地址
将获取到的加载地址当作返回值
这里就要设计到elf文件的文件结构了 使用010打开加载模板
1 | v12 = (baseaddres + *(baseaddres + 28)); |
获取到获取程序头表偏移值52 加载加载的基地址 就是
ELF文件的struct program_header_table
(程序头表)是ELF文件结构中的一个重要组成部分,它定义了程序的“执行时视图”,用于指导加载器如何将程序的各个段加载到内存中。
1 | typedef struct |
程序头表中的每个表项是一个ElfW(Phdr)
结构体,其具体字段如下:
p_type
:段的类型,如PT_LOAD
表示该段需要被加载到内存。p_flags
:段的标志,如PF_R
表示可读,PF_W
表示可写,PF_X
表示可执行。p_offset
:段在文件中的偏移量。p_vaddr
:段在内存中的虚拟地址。p_paddr
:段在物理内存中的地址(通常在现代操作系统中不使用)。p_filesz
:段在文件中的大小。p_memsz
:段在内存中的大小。p_align
:段的对齐方式。
到了
Elf32_Half e_phnum
是一个非常重要的字段,它表示程序头表(Program Header Table)中表项的数量 程序头表描述了ELF文件的各个段(segment),这些段是加载器在加载程序到内存时需要处理的单元。e_phnum
的值告诉加载器程序头表中有多少个段描述符,加载器需要根据这个数量来遍历整个程序头表
判断是否为2的地方是PT_DYNAMIC (2) 作用就是找到struct program_table_entry32_t program_table_element[3]
dd
也就是Dynamic段
Dynamic段是一个包含动态链接信息的段,它被存储在ELF文件的程序头表(Program Header Table)中,通常具有PT_DYNAMIC
类型。这个段在动态链接过程中被动态链接器(如ld-linux.so
)使用,以解析程序的依赖关系和完成动态链接 主要用于寻找与动态链接相关的其他节( .dynsym .dynstr .hash等节)
那我们也就可以知道
这里就是获取各个节的地址
最后一段的标识符就是4 也就是说v7为hash段的偏移地址
继续看就是对字符串进行的操作
手动模拟一下
1 | #include<stdio.h> |
就是根据hash值与nbucket取模作为bucket链的索引,bucket[hash % nbucket]的值作为.dynsym的索引获得动态链接符号表(Elf32_Sym),从符号表的st_name找到.dynstr中对应的字符串与函数名相比较,若不等,则根据bucket[hash % nbucket]的值X作为chain链的索引,chain[X]的值重新获取一个动态链接符号表,拿到字符串索引后获取.dynstr中对应的字符串与函数名相比较,若再不等,继续根据chain[X]的值Y作为chain链的索引,chain[Y]的值重新获取一个动态链接符号表,直到找到或者chain终止为止。代码实现如下
1 | for(i = bucket[funHash % nbucket]; i != 0; i = chain[i]){ |
函数Hash值在74行代码中已经计算得到0x766F8,nbucket=0x107,mod为hash % nbucket = 140,因为hash表的前两个元素是nbucket和nchain,每个元素是Elf32_Word类型,大小为4,所以bucket[hash % nbucket]是第(140 + 2) * 4 = 568号字节,其值为0x19B
0x19B做为.dynsym动态链接符号表(Elf32_Sym)的索引,Elf32_Sym对象大小为16字节,所以在符号表的位置为0x19B * 16 = 6576号字节,st_name是Elf32_Sym对象的第一个元素,所以其值为0x1617
.dynstr字符串表的offset等于0x1D00
st_name为索引的字符串位置则等于0x1D00 + 0x1617 = 0x3317,对应字符串”_ZTIPn”,与ooxx不等。所以需要计算chain[0x19B]的值。先计算chain的起始位置为(nbucket + 2) * 4,nbucket = 0x107,所以chain的起始位置为1060号字节,0x19B十进制为411,那chain链的411索引对应的字节应该是1060 + 411 * 4 = 2704号字节,值为0x5D
对应.dynsym动态链接符号表的位置为0x5D * 16 = 1488号字节,st_name = 0x214
对应的字符串地址为0x1D00 + 0x214 = 0x1F14,字符串值为”ooxx”,是我们需要查找的符号。结合上图,则可知Elf32_Sym对象的st_value = 0x8DC5,st_size = 0x248
牛魔的 复现都要死在这了
现在需要找到的就是key的数值
bss段存放的是需要加载的数据 我们只能从程序运行的内存中dump出内容 进行加解密
先查找出so的加载的基地址再使用ida中的偏移地址获取key
1 | //获取用于异或解密的密钥 |
1 | key = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, |
修复完成